package utils

import (
	"fmt"
	"strings"

	v1core "k8s.io/api/core/v1"
	discovery "k8s.io/api/discovery/v1"
	"k8s.io/client-go/tools/cache"
	"k8s.io/klog/v2"
)

const (
	IPInIPHeaderLength = 20
)

// ServiceForEndpoints given Endpoint object return Service API object if it exists
func ServiceForEndpoints(ci *cache.Indexer, ep *v1core.Endpoints) (interface{}, bool, error) {
	key, err := cache.MetaNamespaceKeyFunc(ep)
	if err != nil {
		return nil, false, err
	}
	klog.V(2).Infof("key for looking up service from Endpoint is: %s", key)

	item, exists, err := (*ci).GetByKey(key)
	if err != nil {
		return nil, false, err
	}

	if !exists {
		return nil, false, nil
	}

	return item, true, nil
}

// ServiceNameforEndpointSlice returns the name of the service that created the EndpointSlice for a given EndpointSlice
//
// With endpoints, the name of the endpoint object always matches the service object, however when it comes to
// EndpointSlices, things work a bit different as k8s' controller will autogenerated it (something like: foo-kl29b)
//
// We can get service information from a number of spots:
// * From the ownerReferences in the metadata EndpointSlice -> metadata -> ownerReferences[0] -> name
// * We can also get this from the label: kubernetes.io/service-name
// * generateName will also contain the prefix for the autogenerated name which should align with our service name
//
// We'll all through all of these and do our best to identify the service's name, if we aren't able to find any of these
// or they disagree with each other we'll throw an error
func ServiceNameforEndpointSlice(es *discovery.EndpointSlice) (string, error) {
	const serviceNameLabel = "kubernetes.io/service-name"
	var ownerRefName, labelSvcName, generateName, finalSvcName string

	ownerRef := es.GetObjectMeta().GetOwnerReferences()
	if len(ownerRef) == 1 {
		ownerRefName = ownerRef[0].Name
	}

	labels := es.GetObjectMeta().GetLabels()
	if svcLabel, ok := labels[serviceNameLabel]; ok {
		labelSvcName = svcLabel
	}

	if es.GetObjectMeta().GetGenerateName() != "" {
		generateName = strings.TrimRight(es.GetObjectMeta().GetGenerateName(), "-")
	}

	if ownerRefName == "" && labelSvcName == "" && generateName == "" {
		return "", fmt.Errorf("all identifiers for service are empty on this EndpointSlice, unable to determine "+
			"owning service for: %s/%s", es.Namespace, es.Name)
	}

	// Take things in an order of precedence here: generateName < ownerRefName < labelSvcName
	finalSvcName = generateName
	if ownerRefName != "" {
		finalSvcName = ownerRefName
	}
	if labelSvcName != "" {
		finalSvcName = labelSvcName
	}

	// At this point we do some checks to ensure that the final owning service name is sane. Specifically, we want to
	// check it against labelSvcName and ownerRefName if they were not blank and return error if they don't agree. We
	// don't worry about generateName as that is less conclusive.
	//
	// From above, we already know that if labelSvcName was not blank then it is equal to finalSvcName, so we only need
	// to worry about ownerRefName
	if ownerRefName != "" && finalSvcName != ownerRefName {
		return "", fmt.Errorf("the ownerReferences field on EndpointSlice (%s) doesn't agree with with the %s label "+
			"(%s) for %s/%s EndpointSlice", ownerRefName, serviceNameLabel, labelSvcName, es.Namespace, es.Name)
	}

	return finalSvcName, nil
}

// ServiceForEndpoints given EndpointSlice object return Service API object if it exists
func ServiceForEndpointSlice(ci *cache.Indexer, es *discovery.EndpointSlice) (interface{}, bool, error) {
	svcName, err := ServiceNameforEndpointSlice(es)
	if err != nil {
		return nil, false, err
	}

	// The key that we're looking for here is just <namespace>/<name>
	key := fmt.Sprintf("%s/%s", es.Namespace, svcName)
	klog.V(2).Infof("key for looking up service from EndpointSlice is: %s", key)

	item, exists, err := (*ci).GetByKey(key)
	if err != nil {
		return nil, false, err
	}

	if !exists {
		return nil, false, nil
	}

	return item, true, nil
}

// ServiceHasNoClusterIP decides whether or not the this service is a headless service which is often useful to
// kube-router as there is no need to execute logic on most headless changes. Function takes a generic interface as its
// input parameter so that it can be used more easily in early processing if needed. If a non-service object is given,
// function will return false.
func ServiceHasNoClusterIP(obj interface{}) bool {
	if svc, _ := obj.(*v1core.Service); svc != nil {
		if svc.Spec.Type == v1core.ServiceTypeClusterIP {
			if ClusterIPIsNone(svc.Spec.ClusterIP) && containsOnlyNone(svc.Spec.ClusterIPs) {
				return true
			}
		}
	}
	return false
}

// ClusterIPIsNone checks to see whether the ClusterIP contains "None" which would indicate that it is headless
func ClusterIPIsNone(clusterIP string) bool {
	return strings.ToLower(clusterIP) == "none"
}

// ClusterIPIsNoneOrBlank checks to see whether the ClusterIP contains "None" or is blank
func ClusterIPIsNoneOrBlank(clusterIP string) bool {
	return ClusterIPIsNone(clusterIP) || clusterIP == ""
}

func containsOnlyNone(clusterIPs []string) bool {
	for _, clusterIP := range clusterIPs {
		if !ClusterIPIsNone(clusterIP) {
			return false
		}
	}
	return true
}
